iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Software Development

文藝復興:這段程式碼,好像有點味道系列 第 20

Day 20|同一張草稿,抄了五份放在不同抽屜:重複的程式碼 (Duplicate Code)

  • 分享至 

  • xImage
  •  

畫室裡,構圖草稿通常只有一份正本

如果同一張草稿,被謄抄了五份,分別放在五個抽屜
日後只要構圖有一點點調整,就得找出全部五份,一份一份修改

漏改一份,這幅畫的五個版本,就再也對不上彼此了

同一條規則,被抄寫了第二次

Day 19 把「免運判斷」整理進了 OrderProcessor

public bool IsEligibleForFreeShipping(OrderRequest request)
{
    bool isVip = request.Tier == CustomerTier.Vip;
    bool meetsStandardThreshold = request.Line.Qty * 100 >= 1000;
    bool meetsConvenienceStoreThreshold =
        request.DeliveryMethod == "超商" &&
        request.Line.Qty * 100 >= 700 &&
        !request.IsFragile;

    return isVip || meetsStandardThreshold || meetsConvenienceStoreThreshold;
}

同一段時間,另一位工程師接到需求:購物車頁面要顯示「還差多少錢免運」的提示

他不知道 OrderProcessor 已經有這條規則,於是在 CartPreviewService 裡,重新寫了一次:

public class CartPreviewService
{
    public string GetFreeShippingHint(Cart cart, Customer customer)
    {
        bool isVip = customer.Tier == CustomerTier.Vip;
        bool meetsStandardThreshold = cart.Qty * 100 >= 1000;
        bool meetsConvenienceStoreThreshold =
            cart.DeliveryMethod == "超商" &&
            cart.Qty * 100 >= 700 &&
            !cart.IsFragile;

        bool isEligible = isVip || meetsStandardThreshold || meetsConvenienceStoreThreshold;

        return isEligible ? "已享免運!" : "再買一點就免運囉";
    }
}

同一條規則,兩個檔案,兩份幾乎一模一樣的邏輯

兩份草稿,遲早會對不上

一個月後,業務又調整規則:VIP 免運的資格,改成需要當月消費滿一次才算數

工程師改了 OrderProcessor.IsEligibleForFreeShipping上線前也記得測試過了,一切正常

CartPreviewService.GetFreeShippingHint 裡的那一份,完全沒有人知道要一起改

結果購物車顯示「已享免運」,結帳時卻被要求付運費
客訴湧入客服信箱,沒有人一開始想得到問題出在兩份「看起來一樣」的規則,悄悄分岔了

兩份草稿,只要有一份被修改而另一份沒有跟上
就會產生看不見的裂縫,直到有人真的去比對兩份草稿,才會發現它們早就不一樣了

把兩個抽屜,合併成一份正本

解法的核心思想:這條規則,只該有一份「真相」,其他所有需要用到它的地方,都只能來查閱,不能自己謄抄

OrderProcessorCartPreviewService 是兩個互不相關的類別
適合用提煉類別 (Extract Class),建立一個新的、專屬的政策類別:

public class ShippingEligibilityPolicy
{
    public bool IsEligibleForFreeShipping(int qty, CustomerTier tier, string deliveryMethod, bool isFragile)
    {
        bool isVip = tier == CustomerTier.Vip;
        bool meetsStandardThreshold = qty * 100 >= 1000;
        bool meetsConvenienceStoreThreshold =
            deliveryMethod == "超商" && qty * 100 >= 700 && !isFragile;

        return isVip || meetsStandardThreshold || meetsConvenienceStoreThreshold;
    }
}

OrderProcessorCartPreviewService,都改成向這個唯一的政策類別詢問答案:

public class OrderProcessor
{
    private readonly ShippingEligibilityPolicy _shippingPolicy;

    public bool IsEligibleForFreeShipping(OrderRequest request) =>
        _shippingPolicy.IsEligibleForFreeShipping(
            request.Line.Qty, request.Tier, request.DeliveryMethod, request.IsFragile);
}

public class CartPreviewService
{
    private readonly ShippingEligibilityPolicy _shippingPolicy;

    public string GetFreeShippingHint(Cart cart, Customer customer)
    {
        bool isEligible = _shippingPolicy.IsEligibleForFreeShipping(
            cart.Qty, customer.Tier, cart.DeliveryMethod, cart.IsFragile);

        return isEligible ? "已享免運!" : "再買一點就免運囉";
    }
}

下一次規則異動,只要改 ShippingEligibilityPolicy 一個地方

購物車的提示,跟結帳時的實際判斷,保證永遠是同一份規則,不會再分岔

重複的程式碼,藏在哪四種地方

今天的例子,重複發生在兩個無關的類別中,但重複其實有好幾種常見的樣貌:

  • 同一個類別裡,兩個方法各自寫了一段相同的邏輯,用提煉方法解決
  • 兩個兄弟子類別裡,各自實作了相同的方法,先各自提煉,再方法上提到父類別
  • if/else 兩個分支裡,都執行了同一段程式碼,把它搬到判斷式外面
  • 兩個完全無關的類別裡(像今天的例子),用提煉類別,建立一個新的共用服務

自我檢查清單

  1. 我正在寫的這段邏輯,讀起來是不是有種「似曾相識」的感覺?
  2. 專案裡有沒有兩個地方,各自實作了幾乎一模一樣的判斷或計算?
  3. 如果要修一個 Bug,是不是得同時修改好幾個檔案裡的相同邏輯?
  4. 這段重複的邏輯,是不是該有一個共同的、唯一的家?
  5. 兩段看似重複的程式碼,是真的代表同一個概念,還是只是巧合地長得像?

明日預告

明天我們看一個相反的極端:一個類別,存在感薄弱到,拿掉它,好像也沒差

模組四第三站:懶惰的類別(Lazy Class)


上一篇
Day 19|畫框上寫著「這裡應該是一朵花」:註解 (Comments)
下一篇
Day 21|領了畫布錢,卻什麼都沒畫的學徒:懶惰的類別 (Lazy Class)
系列文
文藝復興:這段程式碼,好像有點味道27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言